using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Linq;

namespace SharpChannel.Rpc.Proxy
{
    public class ProxyGenerator<T>
	{
		public T ProxyObject{ get; set; }

		public void SetProxyGenerator(IProxyHandler proxyHandler)
		{
			var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly (new AssemblyName ("SharpChannel.Rpc.Proxy"), AssemblyBuilderAccess.Run);
			var moduleBuilder = assemblyBuilder.DefineDynamicModule (assemblyBuilder.GetName ().Name);
			var interfaceType = typeof(T);
			var typeBuilder = moduleBuilder.DefineType ($"{interfaceType.Name}_Proxy", TypeAttributes.Class | TypeAttributes.Public, typeof(object), new[]{interfaceType});
			var objectConstructor = typeof(object).GetConstructor (new Type[]{});
			var proxyFieldBuilder = typeBuilder.DefineField ("handler", typeof(IProxyHandler), FieldAttributes.Private);
			var constructorBuilder = typeBuilder.DefineConstructor (MethodAttributes.Public, CallingConventions.HasThis, new[]{typeof(IProxyHandler)});
			var ilGenerator = constructorBuilder.GetILGenerator ();
			ilGenerator.Emit (OpCodes.Ldarg_0);
		    if (objectConstructor != null) ilGenerator.Emit (OpCodes.Call, objectConstructor);
		    ilGenerator.Emit (OpCodes.Ldarg_0);
			ilGenerator.Emit (OpCodes.Ldarg_1);
			ilGenerator.Emit (OpCodes.Stfld, proxyFieldBuilder);
			ilGenerator.Emit (OpCodes.Ret);

			var proxyMethodInfo = proxyHandler.GetType ().GetMethod ("Invoke", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] {
				//typeof(Object),
				typeof(MethodInfo),
				typeof(object[])
			},
				null);
			foreach (MethodInfo interfaceMethodInfo in interfaceType.GetMethods()) {
                MethodBuilder methodBuilder = null;
                if (interfaceMethodInfo.ContainsGenericParameters)
                {
                   methodBuilder = typeBuilder.DefineMethod (interfaceMethodInfo.Name, MethodAttributes.Public | MethodAttributes.NewSlot | MethodAttributes.Virtual, interfaceMethodInfo.ReturnType, 
                       interfaceMethodInfo.GetParameters ().Select (p => p.ParameterType).ToArray ());
                    methodBuilder.SetReturnType(interfaceMethodInfo.ReturnType);
                    GenericTypeParameterBuilder[] genericParams = methodBuilder.DefineGenericParameters(interfaceMethodInfo.GetGenericArguments().Select(p=>p.Name).ToArray());
                    //methodBuilder.SetParameters(genericParams);
                }
                else
                {
                   methodBuilder = typeBuilder.DefineMethod (interfaceMethodInfo.Name, MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, interfaceMethodInfo.ReturnType, 
                       interfaceMethodInfo.GetParameters ().Select (p => p.ParameterType).ToArray ());

                }
				ILGenerator il = methodBuilder.GetILGenerator ();
				int privateParameterCouunt = interfaceMethodInfo.GetParameters ().Length;
				LocalBuilder argArray = il.DeclareLocal (typeof(object[]));
				il.Emit (OpCodes.Ldc_I4, privateParameterCouunt);
				il.Emit (OpCodes.Newarr, typeof(object));
				il.Emit (OpCodes.Stloc, argArray);
				LocalBuilder methodInfo = il.DeclareLocal (typeof(MethodInfo));
				il.Emit (OpCodes.Ldtoken, interfaceMethodInfo);
				il.Emit (OpCodes.Call, typeof(MethodBase).GetMethod ("GetMethodFromHandle", new[] { typeof(RuntimeMethodHandle) }));
				il.Emit (OpCodes.Stloc, methodInfo);

				for (int i =0; i < interfaceMethodInfo.GetParameters().Length; ++i) {
					ParameterInfo info = interfaceMethodInfo.GetParameters () [i];
					il.Emit (OpCodes.Ldloc, argArray);
					il.Emit (OpCodes.Ldc_I4, i);
					il.Emit (OpCodes.Ldarg_S, i + 1);
					if (info.ParameterType.IsPrimitive || info.ParameterType.IsValueType)
						il.Emit (OpCodes.Box, info.ParameterType);
					il.Emit (OpCodes.Stelem_Ref);
				}
				il.Emit (OpCodes.Ldarg_0);
				il.Emit (OpCodes.Ldfld, proxyFieldBuilder);
				il.Emit (OpCodes.Ldloc, methodInfo);
				il.Emit (OpCodes.Ldloc, argArray);
				il.Emit (OpCodes.Call, proxyMethodInfo);
				if (interfaceMethodInfo.ReturnType.IsValueType && interfaceMethodInfo.ReturnType != typeof(void))
					il.Emit (OpCodes.Unbox_Any, interfaceMethodInfo.ReturnType);
				if (interfaceMethodInfo.ReturnType == typeof(void))
					il.Emit (OpCodes.Pop);
				il.Emit (OpCodes.Ret);

			}
			ProxyObject = (T)Activator.CreateInstance (typeBuilder.CreateType (), proxyHandler);
		}

		public ProxyGenerator (IProxyHandler proxyHandler)
		{
			SetProxyGenerator (proxyHandler);
		}

		public T GetProxyObject()
		{
			return ProxyObject;
		}
	}
}

